Skip to content

[WIP] Show configured report columns as fields in single-expense views - #102457

Open
brunovjk wants to merge 35 commits into
Expensify:mainfrom
brunovjk:issue-101300-customize-fields
Open

brunovjk wants to merge 35 commits into
Expensify:mainfrom
brunovjk:issue-101300-customize-fields

Conversation

@brunovjk

@brunovjk brunovjk commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Explanation of Change

This PR displays configured report columns as read-only fields in workspace expense and single-expense views, while preserving existing expense editors and required fields.

Customize fields reuses the column picker with a separate expense-field preference. Changing expense fields no longer rewrites the report table's columns. Existing table preferences seed additional fields until the expense selection is customized. The picker supports reorder, reset, native accessibility actions and feedback, and grouped native scrolling.

Fixed Issues

$ #101300
PROPOSAL: N/A

Tests

Test Steps

  1. Create a workspace report with an expense. Open the expense and choose More > Customize fields.
  2. With no expense preference, confirm the existing expense editors remain selected. Amount must stay selected.
  3. Add Category GL code and reorder a selected field. Save and reopen: selection and order must persist, without changing the report table's columns.
  4. Deselect an optional field, then leave without saving. Confirm its saved visibility is unchanged.
  5. Use Reset columns and save. Confirm the original expense editors return and extra fields disappear.
  6. Deselect Category or Tag on an expense with a violation or a policy requirement. Confirm the required/error field remains visible.
  7. Verify extra report fields are read-only, Held remains in the header, and paid Amount uses the existing success checkmark color.
  8. In Search on a wide screen, group expenses by Category and open Display > Edit columns.
  9. Reorder Group columns and Expense Columns independently using mouse and keyboard. Save, expand a group, and reopen the picker to verify both orders.
  10. On native, drag a selected row to the last selected position and over unselected rows. Selection must stay unchanged. Check scrolling and native Move up/Move down actions.
  11. Open the picker where Vendor is available and where it is unavailable. Saving in the latter workspace must preserve the preference for the former.
  12. Verify the existing receipt, editing, copy and validation behavior of the expense.

Current revision validation

  • Follow-up: fixed the alphabetical export-key ordering; ExportOnyxState and field-preference suites pass locally (60 tests). Reused useReportTransactions instead of filtering the full transaction collection in the picker. All three updated commits have verified GPG signatures on GitHub.

  • Automated: 410 passing tests across 17 suites, including preference rollback, late-arriving preferences/workspace data, navigation, required fields, keyboard reorder, nested scrolling and HTML selection.

  • TypeScript, React Compiler, formatting and spelling for the PR files pass. Lint for the PR files passes.

  • Interactive: Chrome and iPhone Simulator against staging; save/reopen/cancel/reset, extra-field display, required Category, native drag and simple-list autoscroll, grouped web keyboard reorder and table order verified.

  • Web mouse drag remains inconclusive in automation. Grouped native scrolling on a wide device has not been interactively verified. Android was excluded due to the local SDK setup; physical-device VoiceOver testing was waived by the author.

  • Verify that no errors appear in the JS console

Offline tests

Same as Test Steps

QA Steps

Same as Test Steps

  • Verify that no errors appear in the JS console

PR Author Checklist

  • I linked the correct issue in the ### Fixed Issues section above
  • I wrote clear testing steps that cover the changes made in this PR
    • I added steps for local testing in the Tests section
    • I added steps for the expected offline behavior in the Offline steps section
    • I added steps for Staging and/or Production testing in the QA steps section
    • I added steps to cover failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
    • I tested this PR with a High Traffic account against the staging or production API to ensure there are no regressions (e.g. long loading states that impact usability).
  • I included screenshots or videos for tests on all platforms
  • I ran the tests on all platforms & verified they passed on:
    • Android: Native
    • Android: mWeb Chrome
    • iOS: Native
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • I verified there are no console errors (if there's a console error not related to the PR, report it or open an issue for it to be fixed)
  • I followed proper code patterns (see Reviewing the code)
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I followed the guidelines as stated in the Review Guidelines
  • I tested other components that can be impacted by my changes (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar are working as expected)
  • If a new CSS style is added I verified that:
    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))
  • If new assets were added or existing ones were modified, I verified that:
    • The assets are optimized and compressed (for SVG files, run npm run compress-svg)
    • The assets load correctly across all supported platforms.
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
  • If the PR adds or modifies the UI:
    • I asked an AI agent to review the changes for accessibility issues and addressed its findings.
    • I tested with a screen reader (VoiceOver on macOS) and verified all new/changed elements are reachable with a logical focus order.
    • I verified all new/changed elements have meaningful accessible names and roles.
    • I verified state changes are announced (e.g. checked/unchecked, expanded/collapsed, selected).
  • I added unit tests for any new feature or bug fix in this PR to help automatically prevent regressions in this user flow.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.

Screenshots/Videos

Android: Native
Screen.Recording.2026-10-06.at.10.33.27.mov
Android: mWeb Chrome
iOS: Native
102457_ios_native.mov
iOS: mWeb Safari
MacOS: Chrome / Safari
102457_web_chrome.mov

@melvin-bot

melvin-bot Bot commented Sep 28, 2026

Copy link
Copy Markdown

Hey, I noticed you changed src/languages/en.ts in a PR from a fork. For security reasons, translations are not generated automatically for PRs from forks.

If you want to automatically generate translations for other locales, an Expensify employee will have to:

  1. Look at the code and make sure there are no malicious changes.
  2. Run the Generate static translations GitHub workflow. If you have write access and the K2 extension, you can simply click: [this button]

Alternatively, if you are an external contributor, you can run the translation script locally with your own OpenAI API key. To learn more, try running:

npx bun ./scripts/generateTranslations.ts --help

Typically, you'd want to translate only what you changed by running npx bun ./scripts/generateTranslations.ts --compare-ref main

@github-actions

github-actions Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@codecov

codecov Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ Changes either increased or maintained existing code coverage, great job!

Files with missing lines Coverage Δ
src/CONST/index.ts 95.90% <ø> (ø)
src/ONYXKEYS.ts 100.00% <ø> (ø)
src/ROUTES.ts 34.75% <100.00%> (+0.04%) ⬆️
src/SCREENS.ts 100.00% <ø> (ø)
src/components/ColumnsSettingsListItem.native.tsx 100.00% <100.00%> (ø)
src/components/ColumnsSettingsListItem.tsx 100.00% <100.00%> (ø)
src/components/DraggableList/index.tsx 92.50% <100.00%> (+92.50%) ⬆️
...MenuItem/leaves/content/MenuItemFieldValueHTML.tsx 100.00% <100.00%> (ø)
...portActionItem/MoneyRequestViewAdditionalField.tsx 100.00% <100.00%> (ø)
...ctionItem/MoneyRequestViewFields/BillableField.tsx 100.00% <100.00%> (ø)
... and 24 more
... and 16 files with indirect coverage changes

@brunovjk
brunovjk force-pushed the issue-101300-customize-fields branch 2 times, most recently from 20f4f28 to 7eca905 Compare October 1, 2026 15:20
@brunovjk
brunovjk marked this pull request as ready for review October 2, 2026 12:26
@brunovjk
brunovjk requested review from a team as code owners October 2, 2026 12:26
@melvin-bot
melvin-bot Bot requested review from QichenZhu, flaviadefaria and shawnborton and removed request for a team October 2, 2026 12:26
@melvin-bot

melvin-bot Bot commented Oct 2, 2026

Copy link
Copy Markdown

@shawnborton @QichenZhu One of you needs to copy/paste the Reviewer Checklist from here into a new comment on this PR and complete it. If you have the K2 extension, you can simply click: [this button]

@melvin-bot
melvin-bot Bot removed the request for review from a team October 2, 2026 12:26
Comment thread src/libs/getMoneyRequestViewFields.ts Resolved
@shawnborton

Copy link
Copy Markdown
Contributor

Let us know when this is ready for @Expensify/design review.

@brunovjk brunovjk changed the title Show configured report columns as fields in single-expense views [WIP] Show configured report columns as fields in single-expense views Oct 5, 2026
@brunovjk

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Something went wrong. Try again later by commenting “@codex review”.

An unknown error occurred
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@JS00001

JS00001 commented Oct 26, 2026

Copy link
Copy Markdown
Contributor

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Something went wrong. Try again later by commenting “@codex review”.

Failed to sample tokens
ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@brunovjk

Copy link
Copy Markdown
Contributor Author

@QichenZhu Could you take another look? I believe I’ve addressed all of your comments. Thank you.

@brunovjk brunovjk changed the title [WIP] Show configured report columns as fields in single-expense views Show configured report columns as fields in single-expense views Oct 27, 2026
@QichenZhu

Copy link
Copy Markdown
Contributor

Precondition: Have a USD workspace

Steps:

  1. Create a non-zero expense in USD
  2. Change the amount to 0 in another currency

Expected result: the converted amount shows zero.

Actual result: the converted amount shows the original non-zero amount.

nab.mov

It's not a blocker for this PR, but I think it's a bug worth fixing separately.

Comment on lines +7 to +39
type ColumnItem = {
/** Localized column label, also used as the screen reader name. */
text: string;

/** Column ID passed back when a row is reordered. */
value: SearchCustomColumnIds;

/** Stable row key matching the column ID. */
keyForList: SearchCustomColumnIds;

/** Selected columns appear before the alphabetically ordered unselected columns. */
isSelected: boolean;

/** Required fields cannot be deselected, but can still be reordered. */
isDisabled: boolean;

/** Unselected columns remain visible without becoming drop targets. */
isDragDisabled: boolean;

/** Drag icon wrapped in a touch target by the native variant. */
leftElement: React.JSX.Element;
};

type ColumnsSettingsListItemProps = Pick<DraggableListRenderItemParams<ColumnItem>, 'item' | 'isFocused' | 'drag' | 'isActive'> & {
/** Toggles optional columns without changing the selection of required columns. */
onSelectRow: ListItemProps<ColumnItem>['onSelectRow'];

/** Omitted when this row is already first or cannot be reordered. */
onMoveUp?: () => void;

/** Omitted when the next row is unselected or this row cannot be reordered. */
onMoveDown?: () => void;
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The AI coder understands that comments should add information, which is good, but it misunderstands "information" and puts implementation details into JSDoc.

For example, "Drag icon wrapped in a touch target by the native variant" says which component wraps the icon. It will change over time, and it doesn't explain the prop's purpose or which part of the UI it belongs to. The comment before the refactor was better: "Element rendered on the left side of the list item (e.g. drag handle)".

Another example is "Selected columns appear before the alphabetically ordered unselected columns". It's a detail of only one screen. The comment before the refactor was better since it explained the prop's purpose: the table shows only the selected columns.

type ColumnItem = {
/** Display label for the column */
text: string;
value: SearchCustomColumnIds;
keyForList: SearchCustomColumnIds;
/** Whether the column is currently enabled/visible */
isSelected: boolean;
/** Whether the column toggle is disabled (e.g. for required columns) */
isDisabled: boolean;
/** Whether the column cannot be reordered via drag */
isDragDisabled: boolean;
/** Element rendered on the left side of the list item (e.g. drag handle) */
leftElement: React.JSX.Element;
};
type ColumnsSettingsListProps = {
allColumns: SearchCustomColumnIds[];
/** The default set of selected columns when no customization has been applied */
defaultSelectedColumns: SearchCustomColumnIds[];
/** The currently selected and ordered columns */
currentColumns: SearchCustomColumnIds[];
/** Columns that cannot be deselected by the user */
requiredColumns: Set<SearchCustomColumnIds>;
/** The active group-by field (e.g. category, tag) */
groupBy?: string;
/** The currently selected columns specific to the active group-by mode */
groupColumns?: SearchCustomColumnIds[];
/** The default columns for the active group-by mode when no customization has been applied */
defaultGroupColumns?: SearchCustomColumnIds[];
/** Callback fired with the updated column list when the user saves changes */
onSave: (columns: SearchCustomColumnIds[]) => void;
/** The active Search type, used to resolve type-specific column labels */
type?: SearchDataTypes;
};

Comment thread tests/ui/MoneyRequestViewTest.tsx Outdated
Comment on lines +1661 to +1665
expect(category).toContainElement(hint);
for (let ancestor = hint.parent; ancestor && ancestor !== category; ancestor = ancestor.parent) {
expect(ancestor.type).not.toBe(MenuItem.Row);
expect(ancestor.type).not.toBe(MenuItem.Trailing);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure we need to test the component hierarchy.

>
{title}
</Text>
{!!shouldShowPaid && <Icon src={icons.Checkmark} />}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The checkmark is the wrong colour.

PR

Image

Staging

Image

Comment on lines +55 to +57
case CONST.SEARCH.TABLE_COLUMNS.HELD:
value = translate(isOnHold(transaction) ? 'common.yes' : 'common.no');
break;

@QichenZhu QichenZhu Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The held state can be seen from the header, and we don't have fields for other header info such as Status or From, so I don't think we need a Held field either.

Image

@QichenZhu

Copy link
Copy Markdown
Contributor

@MelvinBot review this again, including accessibility issues

@QichenZhu

Copy link
Copy Markdown
Contributor

@codex review

@MelvinBot

Copy link
Copy Markdown
Contributor

Re-review at b602397, including accessibility

Most of my earlier findings are fixed, and I found no new correctness bugs. Two things are still worth a look before merge: one behaviour item and one new accessibility gap.

1. Customize fields still rewrites the report table's columns (earlier item 3, partly fixed)
Saving now keeps the table-only columns. But if you have no saved preference, the picker still starts from every expense field, and one save writes that whole list (ReportDetailsColumnsPage.tsx:98, :107-136).

  • Example: your report table shows Receipt, Date, Merchant, Category, Tag and Amount. You untick Billable on one expense. The table then adds Description, Card, Tax rate, Tax amount, Attendees and Reimbursable. Data-driven columns also stop appearing on their own.
  • The reverse case: if you've ever saved table columns or pressed Reset, the expense view shows only those columns after deploy. Description, Reimbursable, Billable, Attendees, Tax and Card disappear unless they have a violation (MoneyRequestView.tsx:1263-1292).
  • Suggested fix: with no saved preference, save only the fields the user actually changed. Otherwise, give the expense view its own preference. Either way, please check with product that the shared behaviour is what they want.

2. "Move up" and "Move down" give screen reader users no feedback (accessibility)
The new native actions use translated labels, show only when a move is possible, and reuse the drag reorder path. But after a move, nothing is announced (ColumnsSettingsList.tsx:237-252, ColumnsSettingsListItem.native.tsx:58-64).

  • Example: a VoiceOver user moves "Merchant" down and hears nothing. The row label has no position, so they can't tell where it went.
  • Suggested fix: after onDragEnd(...) in moveColumn, call AccessibilityInfo.announceForAccessibility with a translated string such as "Merchant moved to position 3 of 8". On a device, check that focus stays on the moved row, especially with TalkBack.

Still open from earlier comments: the Held row still renders (MoneyRequestViewAdditionalField.tsx:55-57). I didn't recheck the checkmark colour, JSDoc or test-hierarchy comments.

Status of earlier findings
  • Hidden fields hid violations: fixed. Fields with an error, plus Category and Tag when the policy requires them, are always shown (MoneyRequestView.tsx:1268-1292). One small thing: these rows are added after the saved fields, so a Category with an error shows below Billable.
  • MCC and Exchange rate rows appearing with no preference: fixed. With no preference, the view uses only the default fields.
  • Stale Vendor-only preference: fixed. Vendor is filtered out before the custom check.
  • Alias columns saved twice (Codex P2): fixed. Posted and Total per attendee are swapped back in place of the Date and Attendees rows, so nothing is saved twice.
  • Native drag in the grouped Search columns lists (low): still present. The flat list now sets shouldRestrictDragToEnabledItems, but the two grouped DraggableLists don't (ColumnsSettingsList.tsx:293-321). A selected column dropped among unselected ones snaps back. To fix it, pass the prop to both lists.
  • Native screen reader reorder: fixed, apart from item 2 above.
Accessibility checks with no problems found
  • Read-only report-column rows: they have no onPress, so they aren't exposed as buttons or put in the tab order. An empty value reads as the field name only.
  • Extracted fields: these are EditableField, Merchant, Description, Attendees, Toggle and Billable. Each keeps the same names, roles and error announcements as main. A field gets the button role only when it's editable.
  • Column rows: they keep the checkbox role and checked state. No custom actions clash with the defaults, so a tap still toggles the row.
  • Web: keyboard reordering still uses dnd-kit's KeyboardSensor, which this PR doesn't change.
  • "Customize fields" option: it has translated text and an icon. The page title is translated too.
  • Worth a device check: the row now has onLongPressRow={startDrag}. If TalkBack lists a "long press" action, it could start a drag with no finger on screen, and the row might stay active.

I reviewed the code only. I didn't run a screen reader or test on a device.


view run

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Something went wrong. Try again later by commenting “@codex review”.

Failed to sample tokens
ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@brunovjk

Copy link
Copy Markdown
Contributor Author

Addressing the comments. Thanks for the review.

@github-actions

Copy link
Copy Markdown
Contributor

⚠️ This PR is possibly changing native code and/or updating libraries, it may cause problems with HybridApp. Please check if any patch updates are required in the HybridApp repo and run an AdHoc build to verify that HybridApp will not break. Ask Contributor Plus for help if you are not sure how to handle this. ⚠️

@JS00001

JS00001 commented Oct 30, 2026

Copy link
Copy Markdown
Contributor

back to you @QichenZhu

@brunovjk brunovjk changed the title Show configured report columns as fields in single-expense views feat: show configured report columns as fields in single-expense views Oct 30, 2026
@brunovjk
brunovjk force-pushed the issue-101300-customize-fields branch from 70740e2 to e6a5e1e Compare October 30, 2026 12:37
@brunovjk

Copy link
Copy Markdown
Contributor Author

back to you @QichenZhu

Wait, just a sec, finishing up a few details that were left behind. I’ll let you know when it's ready to continue with the review. Thanks.

@github-actions

Copy link
Copy Markdown
Contributor

This PR adds a new Onyx.get or Onyx.multiGet call, so I've requested a review from the Onyx performance reviewers (@tgolen, @mountiny, @chuckdries, @luacmartins). A review from any one of them is enough. Please check the call against the Onyx.get() rules in ONYX-DATA-MANAGEMENT and ONYX-1.

@brunovjk brunovjk changed the title feat: show configured report columns as fields in single-expense views [WIP] Show configured report columns as fields in single-expense views Oct 30, 2026
@brunovjk

Copy link
Copy Markdown
Contributor Author

The PR turned out a bit larger than I had anticipated; I suspect I might be touching areas that don't actually need changes. I’m reviewing it carefully and will let you know as soon as it’s ready for review. Thanks.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants